Skip to content

Migrate the Site Creation Domain Search to wordpress-rs - #23247

Merged
oguzkocer merged 11 commits into
trunkfrom
integrate-wordpress-rs-site-creation-domains
Aug 26, 2026
Merged

Migrate the Site Creation Domain Search to wordpress-rs#23247
oguzkocer merged 11 commits into
trunkfrom
integrate-wordpress-rs-site-creation-domains

Conversation

@oguzkocer

@oguzkocer oguzkocer commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Description

Moves SiteCreationDomainsViewModel and FetchDomainsUseCase off FluxC and onto wordpress-rs. FetchDomainsUseCase calls WpComApiClient.domains().suggestions() directly instead of dispatching newSuggestDomainsAction and waiting for an OnSuggestedDomains event, so the EventBus subscription, the query-to-continuation pairing, and the ViewModel's dispatcher registration all go away.

Behaviour is meant to match trunk. The pieces worth checking:

  • empty_results is not a failure. The API reports "no domains for that search" as an HTTP error, and FluxC turned it back into a successful empty list. The migration does the same, so the screen shows the empty-list message rather than a red error with a retry button.
  • invalid_query keeps its own message. Same as trunk, and it is not tracked as an error.
  • The tracked error type survives. FetchDomainsResult.Error carries the API's error code and message through to trackErrorShown. SuggestDomainErrorType supplied that value before, derived from the same code, and the tracker lowercases whatever it is given, so the value reported for a known code is unchanged.

Three cleanups came out of reviewing the migration:

  • fetchDomains had a size parameter that was suppressed rather than read, since quantity is always FETCH_DOMAINS_SIZE. No caller passed it.
  • Every FetchDomainsResult echoed the query back, which the caller already held. That existed to match an EventBus result to its request and a suspend call needs no such thing.
  • The screen fetched the domain products list on start and stored it in a field nothing read. NewDomainsSearchRepository and DomainSuggestionsViewModel are the callers that read a product, both for combinedSaleCostDisplay, and neither is affected. The sale UI this screen kept for it — Cost.OnSale, Tag.Sale, the SalePrice composable — was only ever built by DomainItemPreview, so it rendered in Android Studio and nowhere else.

Dropping animateLayoutChanges on this screen

On trunk, typing the first character into the domain search stops you typing until that first request finishes — every keystroke in between is lost. The implicit layout animation that runs when the clear button and the spinner appear takes focus off the search field, and nothing gives it back until the results arrive.

Removing animateLayoutChanges from this screen's two layouts fixes it. The trade is that the views around the header snap into place rather than sliding. It reads as snappier to me and losing keystrokes was the worse of the two, but it is a visible change.

Testing instructions

Reach the screen: My Site → the site name dropdown → +Create WordPress.com site → skip or fill the two steps before Choose a domain.

Typing is not interrupted:

  1. Type a single character into the search field.
  • Verify you can keep typing straight away, without waiting for the spinner to stop or tapping the field again.

A query the API refuses:

  1. Search for .
  • Verify the empty-state message about the search being invalid, not an error with a retry button.

A query with no matches:

  1. Search for a long run of the same letter, e.g. 200 or so z characters.
  • Verify the ordinary empty-state message.

A normal search:

  1. Search for anything ordinary, e.g. coolsite.
  • Verify suggestions appear, the free .wordpress.com option among them.
  • Verify the first two paid suggestions are tagged Recommended and Best Alternative.
  • Verify selecting one enables the button at the bottom and its label matches free vs. paid.

No connection:

  1. Turn off networking and search.
  • Verify the no-network message with a retry button, and that retry works once networking is back.

Screenshots

Invalid query No matches Results No connection
Invalid query No matches Results No connection

@dangermattic

dangermattic commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator
2 Warnings
⚠️ This PR is larger than 300 lines of changes. Please consider splitting it into smaller PRs for easier and faster reviews.
⚠️ PR is not assigned to a milestone.

Generated by 🚫 Danger

@wpmobilebot

wpmobilebot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

App Icon📲 You can test the changes from this Pull Request in Jetpack Android by scanning the QR code below to install the corresponding build.

App NameJetpack Android
Build TypeDebug
Versionpr23247-784fb8d
Build Number1498
Application IDcom.jetpack.android.prealpha
Commit784fb8d
Installation URL4c63ns520cvn0
Automatticians: You can use our internal self-serve MC tool to give yourself access to those builds if needed.

@wpmobilebot

wpmobilebot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

App Icon📲 You can test the changes from this Pull Request in WordPress Android by scanning the QR code below to install the corresponding build.

App NameWordPress Android
Build TypeDebug
Versionpr23247-784fb8d
Build Number1498
Application IDorg.wordpress.android.prealpha
Commit784fb8d
Installation URL2iuokunddobu0
Automatticians: You can use our internal self-serve MC tool to give yourself access to those builds if needed.

@wpmobilebot

Copy link
Copy Markdown
Contributor

🤖 Build Failure Analysis

This build has failures. Claude has analyzed them - check the build annotations for details.

@codecov

codecov Bot commented Aug 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.94595% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 37.82%. Comparing base (4fba2e4) to head (784fb8d).
⚠️ Report is 1 commits behind head on trunk.

Files with missing lines Patch % Lines
...id/ui/sitecreation/usecases/FetchDomainsUseCase.kt 92.10% 0 Missing and 3 partials ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##            trunk   #23247   +/-   ##
=======================================
  Coverage   37.81%   37.82%           
=======================================
  Files        2347     2347           
  Lines      128360   128362    +2     
  Branches    17770    17774    +4     
=======================================
+ Hits        48542    48555   +13     
+ Misses      75861    75851   -10     
+ Partials     3957     3956    -1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@oguzkocer
oguzkocer marked this pull request as ready for review August 20, 2026 22:13
@oguzkocer
oguzkocer requested review from a team and adalpari and removed request for a team August 20, 2026 22:13
oguzkocer added a commit that referenced this pull request Aug 23, 2026
Typing the first character hides the intro block and swaps the search icon
for a spinner. `animateLayoutChanges` animated the resulting bounds change,
which is what makes the screen jump and the keyboard flicker while the first
results load.

The views around the header now snap rather than animating. #23247 drops the
attribute from the site creation search, where the same animation was taking
focus off the search field.
@adalpari

adalpari commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

There seem to be a few points it would be great if you could double-check:

🔴 Crash risk

  1. FetchDomainsUseCase.kt:27 — Domain search can crash the app
    The new code calls getOrCreateClient() which does requireNotNull(accountStore.accessToken). If the token is null/cleared/expired during a domain search, IllegalStateException is thrown inside SiteCreationDomainsViewModel.fetchDomains’s launch{} (line 126). That scope uses a plain Job() (non-supervisor) with no CoroutineExceptionHandler and no try/catch, so the exception crashes the app. The old code returned OnSuggestedDomains with isError and surfaced a UI error instead.

🟡 Robustness / UX

  1. FetchDomainsUseCase.kt:26 — Stale API client
    getOrCreateClient caches the WpComApiClient once and never rebuilds it on token rotation. A refreshed token within the same process is ignored, causing auth failures until restart.

  2. PostRsListViewModel.kt:176 — Offline change never re-applied
    A change detected while the screen is visible but offline sets hasDeferredChange=true, but nothing re-triggers when connectivity returns (only onScreenVisible does). List stays stale until the user leaves and returns. Same in PagesRsListViewModel.

  3. PostRsListViewModel.kt:832 — Lost pull-to-refresh error feedback
    A user pull-to-refresh deferred while a background refresh runs is replayed via refreshTab(tab) with the default isUserRefresh=false. If the replay fails, the user who explicitly pulled sees no error snackbar. Same in PagesRsListViewModel.

  4. ViewsStatsViewModel.kt:329 — Title/data mismatch on metric tap
    onMetricSelected updates the title/selectedMetric while keeping a previous period’s Loaded chart when lastChartResult is null, briefly showing the new metric label over the old period’s data.

  5. ViewsStatsViewModel.kt:306 — Non-atomic config save
    saveMetric does a read-modify-write of the stats card config in a launched coroutine. Rapid metric taps can interleave reads and clobber each other or revert concurrent changes to other config fields.

oguzkocer added a commit that referenced this pull request Aug 25, 2026
Typing the first character hides the intro block and swaps the search icon
for a spinner. `animateLayoutChanges` animated the resulting bounds change,
which is what makes the screen jump and the keyboard flicker while the first
results load.

The views around the header now snap rather than animating. #23247 drops the
attribute from the site creation search, where the same animation was taking
focus off the search field.
oguzkocer added a commit that referenced this pull request Aug 25, 2026
* Migrate `DomainSuggestionsViewModel` from FluxC to wordpress-rs

Fetch domain products and suggestions through `WpComApiClient` instead of
`ProductsStore` and the `Dispatcher`/EventBus pair. Suspend calls remove the
need for the `@Subscribe` handler and the dispatcher registration.

Changes:
- Swap `ProductsStore` and `Dispatcher` for `WpComApiClientProvider` and
  `AccountStore`
- Map the `empty_results` rejection to an empty success, as FluxC's
  `SiteRestClient.suggestDomains` did
- Carry the API's message into `ListState.Error`, and log failures with
  `toLogErrorString`
- Retarget `Product.isOnSale` at the wordpress-rs `Product`, whose `saleCost`
  holds hundredths of the currency unit rather than a decimal amount
- Extract `buildSuggestionsParams`, and cover the sale, error and superseded
  query paths

* Cover the product type the suggestions screen requests

Extract `buildProductsParams` so the filter can be asserted. `request` takes
an opaque lambda, so a verify on the client counts calls without pinning
which endpoint each one reached: swapping `ProductTypeFilter.Domains` for
another filter left every test green while no suggestion would carry a sale
price.

Changes:
- Extract `buildProductsParams` and assert it selects `Domains`
- Rename `domain products are fetched only at first start` to name the
  repeat-start guarantee its call count actually covers

* Bind each test observer to the list it was created with

The observers closed over the `onDomainSelectedEvents` and `suggestionStates`
fields, which `createViewModel` reassigns. A test that builds a second view
model leaves the first one's observers writing into the second one's lists.

* Name the non-API failure test after what it asserts

The fallback to `R.string.domain_suggestions_fetch_error` happens in
`DomainSuggestionsFragment`, which this test does not exercise. It asserts
that `ListState.Error` carries no message.

* Handle the suggestions response on the main thread

`suggestions` is a plain field written from the search field, the debouncer
and the response handler. FluxC pinned the handler to the main thread through
`@Subscribe(threadMode = ThreadMode.MAIN)`; the coroutine that replaced it
runs on `BG_THREAD`, so a response landing while `fetchSuggestions` is still
running can be overwritten by the stale state that thread holds. `postValue`
then coalesces the two, leaving the spinner up with no error reported.

A request that fails without reaching the network, as it does with no
connection, is fast enough to make the overlap likely.

Changes:
- Inject the `UI_THREAD` dispatcher and run `onDomainSuggestionsFetched` on it

* Tell the user when a domain search failed for want of a connection

The screen reported every failure as "Domain suggestions couldn't be loaded",
including the one the user can act on. wordpress-rs distinguishes a request
that never reached the network, so an offline search now says to check the
connection.

Changes:
- Set `errorMessageResId` to `error_network_connection` when the result is a
  `DeviceIsOfflineError`
- Prefer `errorMessageResId` over `errorMessage` in the fragment's toast

* Drop the implicit layout animation from the domain search

Typing the first character hides the intro block and swaps the search icon
for a spinner. `animateLayoutChanges` animated the resulting bounds change,
which is what makes the screen jump and the keyboard flicker while the first
results load.

The views around the header now snap rather than animating. #23247 drops the
attribute from the site creation search, where the same animation was taking
focus off the search field.

* Report a failed domain search once

A failed search with nothing already on screen showed two messages at once:
the empty view said no domains were found while a toast said the suggestions
could not be loaded. The empty view is attached to the list through
`setEmptyView`, so it appears for any empty list including the one an error
leaves behind.

The failure now goes into the empty view when the list is empty, with the
reason as its subtitle, and falls back to a toast only when there are still
results on screen to keep.

* Start the domain search at rest rather than loading

The layout shipped the search icon `gone` and the progress bar visible, the
inverse of the `tools:visibility` on each. The fragment corrects both the
first time `suggestions` is assigned, so any path that reaches the screen
without issuing a search leaves the spinner running with nothing behind it.

* Clear the suggestions when a search fails

The list answers the query in the search field, and `ListState.Error` is
built from the previous state's data. Rejecting a query therefore left the
previous query's results on screen beneath the new text, so searching `...`
after a real search showed that search's domains as though they answered it.

The results are already hidden while a request is in flight, so this stops a
failure restoring them rather than introducing a new behaviour. With the list
always empty on failure, the empty view carries every error and the toast goes
away, which also stops the longer API messages being truncated.

* Clear the search results when the field is emptied

Emptying the field left the last result on screen, so an error explaining how
a query has to be written stayed up after that query was deleted. Clearing
now returns the list to `Init`, the state the screen opens in, and the
fragment hides the list and its empty view there as it does while loading.

The default search that follows an emptied field is unchanged, so the site's
own suggestions still come back when they were not already showing.

* Stop searching for a blank query

A site with no name gives `initializeDefaultSuggestions` nothing to search
for, on open and again whenever the field is emptied. The blank query went to
the API, which rejects it as `invalid_query`, so emptying the field replaced
the error it was meant to clear with an identical one.

A blank query now leaves the list in `Init` without a request, which is the
resting state the field's placeholder already implies.

* Keep the suggestions through a configuration change

Restoring the view state sets the search field's text, and `TextView` reports
that as a change even when the text is empty, so a rotation tells the view
model the query was cleared. Clearing reset the list to `Init`, and no fetch
follows when the query is already the site name, so the retained suggestions
were replaced by a blank screen with no way back but typing.

Emptying the field now only resets the list when the query has moved off the
site name, which is the case the reset exists for.

* Clear the selected domain when no search is issued

`fetchSuggestions` clears the selection at the end, and the blank-query guard
returns before reaching it. Emptying the field on a site with no name left
the Select button enabled over a list that had just been cleared, holding a
domain the user could no longer see, and tapping it created a cart.

The reset also moves above the request, so every write this thread makes to
`suggestions` is done before a response can arrive on another one. The
comment on the response handler claimed that as an effect of running on the
main thread, which it never was — `fetchSuggestions` runs on the debouncer's
scheduler, so the two threads overlapped either way.

* Drop part of a comment in DomainSuggestionsViewModel that referred an earlier implementation

* Report a missing WP.com account rather than assuming one

`AccountStore.accessToken` is typed nullable but reads `""` when signed out,
and is only null between an in-process sign out and the next launch. A null
check therefore passed in the case it was written for and the screen sent
`Authorization: Bearer ` for the server to reject, which arrived as the same
generic failure as a dropped connection. In the case it did catch it threw
inside `launch`, where nothing handles it.

`getOrCreateClient` now returns null for either shape, matching
`FetchAllDomainsUseCase`, and both requests report it instead: the product
fetch logs and carries on, and the search shows the error the fragment
already has a string for.

* Write the suggestions state from one thread

`suggestions` was read-modify-written from three threads: the debouncer's
scheduler in `fetchSuggestions`, the main thread from the row taps and the
response handler, and `BG_THREAD` from `showLoadingButton` during cart
creation. Tapping Select as a response landed could interleave two of those
cycles, dropping the fetched list or leaving rows enabled mid-purchase.

`fetchSuggestions` and `showLoadingButton` now run on the main thread with
the rest. The query is still read on the debouncer thread and passed in, so
it remains the one the search was scheduled for rather than whatever the
field holds by the time the main thread gets there.
@oguzkocer
oguzkocer force-pushed the integrate-wordpress-rs-site-creation-domains branch from 32d2744 to cc6f672 Compare August 25, 2026 22:44
@oguzkocer

Copy link
Copy Markdown
Contributor Author

@adalpari Thank you for the review.

  1. Following the same explanation I left in Migrate the Domain Suggestions Screen to wordpress-rs #23255, I've softened the handling of missing access token in cc6f672.

  2. This is common to many other features at the moment and once we consolidate our WpComApiClient provider approach, we could make some improvements to it. Having said that, it's not something we should see in practice, because you can't logout/change your token while in these flows. In any case, this is not a feature implementation level change and should be tackled on its own.

3-6. I'm confused. These items have nothing to do with this PR, do they?

@adalpari

Copy link
Copy Markdown
Contributor

3-6. I'm confused. These items have nothing to do with this PR, do they?

You are totally right. I used the full diff and ended up getting those points as well. Those ones are just UX suggestions anyway, so I think it's fine.

@adalpari adalpari left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for checking my comment. LGTM!

… FluxC to wordpress-rs

Replace FluxC's dispatcher/EventBus domain suggestions with direct
wordpress-rs API calls:

- `FetchDomainsUseCase` now uses `WpComApiClient.domains().suggestions()`
  instead of FluxC's `Dispatcher` + `OnSuggestedDomains` event
- `SiteCreationDomainsViewModel` uses wordpress-rs for products fetch
  instead of `ProductsStore`
- Handle `invalid_query` API error via `WpErrorCode.CustomException`
  to preserve the empty-results UX (matching FluxC's `INVALID_QUERY`)
- Remove `Dispatcher` registration/unregistration from ViewModel
`quantity` is always `FETCH_DOMAINS_SIZE`, so the parameter was suppressed
rather than read. No caller passed it.

Changes:
- Remove `size` from `FetchDomainsUseCase.fetchDomains`
- Drop it from the stubs and verifications in `SiteCreationDomainsViewModelTest`
The caller passes the query in and holds it across the call, so it never
read the copy coming back. Echoing it was needed when the result arrived
over EventBus and had to be matched to its request.

Changes:
- Remove `query` from `FetchDomainsResult`, making `InvalidQuery` and
  `Error` data objects
- Update the ViewModel branches and both test classes
Every rejection reported as a generic error with no message, and
`empty_results` reached the user as a failure with a retry button rather
than the empty-list message it stands for.

`SuggestDomainErrorType` supplied the tracked type before, derived from the
same API code; the tracker lowercases what it is given, so the code goes
through as-is.

Changes:
- Carry the API error code and message on `FetchDomainsResult.Error` and
  track them instead of a hardcoded `GENERIC_ERROR`
- Map `empty_results` to an empty `Success`
- Log the product-fetch failure with `toLogErrorString()`
- Cover the `invalid_query`, `empty_results`, and other-code paths, and
  assert the tracked type and message
Changes:
- Sort the `rs.wordpress` and `uniffi` imports after the `org.wordpress`
  block in the ViewModel and its test, matching the other migrated classes
- Make `SALE_PRODUCTS_COUNT` a const and drop its `@Suppress("unused")`;
  both sale tests read it
- Drop an `@Suppress("UNCHECKED_CAST")` from a test with no cast, and a
  stray blank line
`SiteCreationDomainsViewModel` requested the products list on start and
stored it in a field nothing read, so the screen paid for a request it made
no use of.

`NewDomainsSearchRepository` and `DomainSuggestionsViewModel` are the callers
that read a product, both for `combinedSaleCostDisplay`, and neither is
affected.

Changes:
- Remove the products fetch, the cached field, and the API client it needed,
  along with the `WpComApiClientProvider` and `AccountStore` dependencies
- Remove the test asserting the fetch happened once, and the two `@Ignore`d
  sale tests it fed
`Cost.OnSale` and `Tag.Sale` were only ever built by `DomainItemPreview`, so
the sale price and its tag rendered in Android Studio and nowhere else.

Changes:
- Remove `Cost.OnSale`, `Tag.Sale`, the `SalePrice` composable, and the
  branch selecting it
- Remove them from the preview fixture
- Remove the `site_creation_domain_cost_sale` and
  `site_creation_domain_tag_sale` strings they used
Typing the first character made the clear button and the spinner appear,
and the `LayoutTransition` animated the neighbouring views' bounds through
zero height. The framework clears focus on a view animated to zero size, so
the field lost it, the toolbar's navigation button took it, and the input
connection stayed inactive until the results arrived and the field asked for
focus back. Every character typed in between was dropped.

Both layouts belong to this screen alone. The header still animates itself
in `updateHeader`; what is lost is the implicit animation of the views
around it.

Changes:
- Drop `android:animateLayoutChanges` from `site_creation_domains_screen`
  and `site_creation_search_input_item`
Checkstyle rejects an empty line before a closing brace, and removing
`Tag.Sale` and `Cost.OnSale` left one behind in each.

Changes:
- Drop the blank lines before the `Tag` and `Cost` closing braces
Lint's `ExtraTranslation` rejects a string that a locale translates but the
default locale does not define, and the two sale strings left the default
locale with the UI that used them.

Changes:
- Delete `site_creation_domain_tag_sale` and
  `site_creation_domain_cost_sale` from every `values-*/strings.xml`
`AccountStore.accessToken` is typed nullable but reads `""` when signed out,
and is only null between an in-process sign out and the next launch. A null
check therefore passed in the case it was written for and the search sent
`Authorization: Bearer ` for the server to reject, which arrived as the same
generic failure as a dropped connection. In the case it did catch it threw
inside `launch`, where nothing handles it.

`getOrCreateClient` now returns null for either shape, matching
`FetchAllDomainsUseCase`, and the search reports it as its own error type so
it stays separable from a request that reached the server.

Changes:
- Return null from `getOrCreateClient` for a null or empty token
- Report `NO_ACCESS_TOKEN` from `fetchDomains`, and log it
- Cover both token shapes
@oguzkocer
oguzkocer force-pushed the integrate-wordpress-rs-site-creation-domains branch from cc6f672 to 784fb8d Compare August 26, 2026 09:26
@oguzkocer
oguzkocer enabled auto-merge (squash) August 26, 2026 09:26
@oguzkocer
oguzkocer merged commit 68b05e3 into trunk Aug 26, 2026
21 of 23 checks passed
@oguzkocer
oguzkocer deleted the integrate-wordpress-rs-site-creation-domains branch August 26, 2026 09:39
oguzkocer added a commit that referenced this pull request Aug 26, 2026
`getOrCreateClient` treats a null and an empty `accessToken` alike, which reads
as a redundant check without knowing that `AccountStore` uses the two to mean
the same thing. Documents it, matching #23247 and #23255.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants